SPB Git forge
7commits 1branches 0releases
229.0 KBsize
maindefault branch
12 days agolast push
TypeScript 91.8% HTML 3.2% JavaScript 3% SQL 1.4% CSS 0.7%
10.5 KB · 199 lines tsx
Raw Blame History
1"use client";23import { use } from "react";4import Link from "next/link";5import { useApi } from "@/lib/api";6import { fmtDate } from "@/lib/format";7import { Bar, Empty, Metric, Panel, Skeleton, Table, Tag } from "@/components/ui";89interface Detail {10  platform: string;11  summary: { confidence: number; page_types: number; entity_types: number; navigation_actions: number; network_schemas: number; media_patterns: number; sessions: number } | null;12  model: {13    updated_at: string;14    response_patterns: Record<string, { shape_hash: string; hostname: string; path_pattern: string; method: string; graphql_operation?: string; likely_entity_types: Record<string, number>; observed_count: number; entity_yield_total: number; confidence: number; triggered_by: Record<string, number>; first_seen: string; last_seen: string }>;15    action_patterns: Record<string, { action_type: string; from_page_type: string; observed_count: number; avg_new_entities: number; avg_network_responses: number; to_page_types: Record<string, number>; failures: number }>;16    page_types: Record<string, { page_type: string; observed_count: number; url_patterns: Record<string, number>; avg_entities: number; entity_types: Record<string, number>; avg_confidence: number }>;17    media_patterns: Record<string, { hostname: string; kind: string; observed_count: number }>;18    sessions_learned: string[];19  } | null;20  manifest: { compiled_at: string; confidence: number; network_patterns: unknown[] } | null;21  profiles: { alias: string; has_state: boolean }[];22}2324export default function PlatformPage({ params }: { params: Promise<{ platform: string }> }) {25  const { platform } = use(params);26  const { data, loading } = useApi<Detail>(`/platforms/${platform}`, { refreshMs: 15_000 });27  if (loading) return <Skeleton rows={10} />;28  if (!data) return <Empty>unknown platform</Empty>;29  const m = data.model;30  const rps = m ? Object.values(m.response_patterns).filter((r) => Object.keys(r.likely_entity_types).length).sort((a, b) => b.confidence - a.confidence) : [];31  const aps = m ? Object.values(m.action_patterns).filter((a) => a.from_page_type !== "ANY").sort((a, b) => b.observed_count - a.observed_count) : [];32  const pts = m ? Object.values(m.page_types).sort((a, b) => b.observed_count - a.observed_count) : [];33  return (34    <div className="space-y-4">35      <div className="text-xs text-dim mono">36        <Link href="/platforms" className="hover:text-cyan">connectors</Link> / {platform}37      </div>38      <div className="flex flex-wrap items-end gap-4">39        <h1 className="text-2xl font-semibold tracking-tight capitalize">{platform}</h1>40        {data.summary && (41          <div className="ml-auto flex items-center gap-3 w-72">42            <span className="text-xs text-dim">confidence</span>43            <Bar value={data.summary.confidence / 100} className="flex-1" color={data.summary.confidence >= 70 ? "#43e69a" : "#38e1ff"} />44            <span className="mono">{data.summary.confidence}%</span>45          </div>46        )}47      </div>48      {!m ? (49        <Panel>50          <Empty>51            Nothing learned yet on {platform}. {["youtube", "reddit"].includes(platform) ? "Launch a learning mission from the Missions page." : "No adapter yet — this platform is scheduled for a later phase."}52          </Empty>53        </Panel>54      ) : (55        <>56          <div className="grid grid-cols-1 xl:grid-cols-3 gap-4">57            <Panel title="Learned model">58              <Metric label="page types" value={data.summary?.page_types} />59              <Metric label="entity types" value={data.summary?.entity_types} />60              <Metric label="navigation actions" value={data.summary?.navigation_actions} />61              <Metric label="network schemas" value={data.summary?.network_schemas} />62              <Metric label="media patterns" value={data.summary?.media_patterns} />63              <Metric label="sessions learned from" value={m.sessions_learned.length} />64              <Metric label="updated" value={fmtDate(m.updated_at)} />65              <Metric label="compiled manifest" value={data.manifest ? `${fmtDate(data.manifest.compiled_at)} · ${data.manifest.network_patterns.length} patterns` : "not yet (needs ≥ 40 %)"} />66              <Metric label="browser profiles" value={data.profiles.length ? data.profiles.map((p) => `${p.alias}${p.has_state ? " ✓" : ""}`).join(", ") : "none — run pnpm login"} />67            </Panel>68            <Panel title="Page types" sub="what the crawler recognises, and what each page usually yields" className="xl:col-span-2" pad={false}>69              <div className="p-4">70                <Table head={["page type", "seen", "avg entities", "avg conf", "url patterns", "entity mix"]} dense>71                  {pts.map((p) => (72                    <tr key={p.page_type}>73                      <td>74                        <Tag color="#38e1ff">{p.page_type}</Tag>75                      </td>76                      <td className="mono">{p.observed_count}</td>77                      <td className="mono">{p.avg_entities.toFixed(1)}</td>78                      <td className="mono">{p.avg_confidence.toFixed(2)}</td>79                      <td className="mono text-[11px] text-fg-2">80                        {Object.entries(p.url_patterns)81                          .sort((a, b) => b[1] - a[1])82                          .slice(0, 3)83                          .map(([k, v]) => `${k} (${v})`)84                          .join("  ")}85                      </td>86                      <td className="text-[11px]">87                        {Object.entries(p.entity_types)88                          .sort((a, b) => b[1] - a[1])89                          .slice(0, 4)90                          .map(([k, v]) => (91                            <Tag key={k} className="mr-1">92                              {k} {v}93                            </Tag>94                          ))}95                      </td>96                    </tr>97                  ))}98                </Table>99              </div>100            </Panel>101          </div>102          <Panel title="Runtime API map" sub="action → response shape → entity types (§71) — the learned frontend API of the platform" pad={false}>103            <div className="p-4">104              {rps.length ? (105                <Table head={["endpoint", "entity types", "triggered by", "seen", "yield", "confidence"]} dense>106                  {rps.map((r) => (107                    <tr key={r.shape_hash}>108                      <td className="mono text-[11.5px]">109                        <Tag color="#9d7bff" className="mr-1">110                          {r.method}111                        </Tag>112                        {r.hostname}113                        <span className="text-fg-2">{r.path_pattern}</span>114                        {r.graphql_operation && <Tag color="#ff7ad9" className="ml-1">{r.graphql_operation}</Tag>}115                        <div className="text-[10px] text-dim">#{r.shape_hash.slice(0, 10)}</div>116                      </td>117                      <td>118                        {Object.entries(r.likely_entity_types).map(([t, n]) => (119                          <Tag key={t} color="#38e1ff" className="mr-1">120                            {t} {n}121                          </Tag>122                        ))}123                      </td>124                      <td>125                        {Object.entries(r.triggered_by).map(([a, n]) => (126                          <Tag key={a} color="#ffb347" className="mr-1">127                            {a.toLowerCase()} {n}128                          </Tag>129                        ))}130                      </td>131                      <td className="mono">{r.observed_count}</td>132                      <td className="mono">{(r.entity_yield_total / Math.max(1, r.observed_count)).toFixed(1)} ent/resp</td>133                      <td className="w-32">134                        <Bar value={r.confidence} color={r.confidence >= 0.5 ? "#43e69a" : "#ffb347"} />135                        <div className="mono text-[10px] text-dim">{r.confidence.toFixed(2)}</div>136                      </td>137                    </tr>138                  ))}139                </Table>140              ) : (141                <Empty>no entity-bearing schema learned yet</Empty>142              )}143            </div>144          </Panel>145          <div className="grid grid-cols-1 xl:grid-cols-2 gap-4">146            <Panel title="Navigation patterns" sub="action from a page type → where it leads and what it yields (§30)" pad={false}>147              <div className="p-4">148                <Table head={["action", "from", "→ to", "seen", "avg new ent", "avg resp", "fail"]} dense>149                  {aps.map((a) => (150                    <tr key={a.action_type + a.from_page_type}>151                      <td>152                        <Tag color="#ffb347">{a.action_type.toLowerCase()}</Tag>153                      </td>154                      <td>155                        <Tag color="#38e1ff">{a.from_page_type}</Tag>156                      </td>157                      <td className="text-[11px]">158                        {Object.entries(a.to_page_types)159                          .sort((x, y) => y[1] - x[1])160                          .map(([k, v]) => `${k} ${v}`)161                          .join(", ")}162                      </td>163                      <td className="mono">{a.observed_count}</td>164                      <td className="mono">{a.avg_new_entities.toFixed(1)}</td>165                      <td className="mono">{a.avg_network_responses.toFixed(1)}</td>166                      <td className="mono">{a.failures}</td>167                    </tr>168                  ))}169                </Table>170              </div>171            </Panel>172            <Panel title="Media delivery patterns" pad={false}>173              <div className="p-4">174                {Object.values(m.media_patterns).length ? (175                  <Table head={["host", "kind", "seen"]} dense>176                    {Object.values(m.media_patterns)177                      .sort((a, b) => b.observed_count - a.observed_count)178                      .map((mp) => (179                        <tr key={mp.hostname + mp.kind}>180                          <td className="mono text-[11.5px]">{mp.hostname}</td>181                          <td>182                            <Tag color="#ff7ad9">{mp.kind}</Tag>183                          </td>184                          <td className="mono">{mp.observed_count}</td>185                        </tr>186                      ))}187                  </Table>188                ) : (189                  <Empty>none yet</Empty>190                )}191              </div>192            </Panel>193          </div>194        </>195      )}196    </div>197  );198}199